/*
 * Copyright (c) 2012 swing-on-fire Team
 * 
 * This file is part of Swing-On-Fire (http://code.google.com/p/swing-on-fire), licensed under the terms of the MIT
 * License (MIT).
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
 * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
 * Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
 * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
 * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
package com.google.code.swingonfire.utility;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.Map;

public class Accessor
{

    public static Accessor forMethod(String fieldOrMethod, Class<?>... parameterTypes)
    {
        return new Accessor(forStack(fieldOrMethod), fieldOrMethod, false, false, parameterTypes);
    }

    public static Accessor forGetter(String fieldOrMethod)
    {
        return new Accessor(forStack(fieldOrMethod), fieldOrMethod, true, false);
    }

    public static Accessor forGetter(String fieldOrMethod, Class<?> returnType)
    {
        return new Accessor(forStack(fieldOrMethod), fieldOrMethod, true, false);
    }

    public static Accessor forSetter(String fieldOrMethod, Class<?>... parameterTypes)
    {
        return new Accessor(forStack(fieldOrMethod), fieldOrMethod, false, true, parameterTypes);
    }

    private static Accessor forStack(String fieldOrMethod)
    {
        int index = fieldOrMethod.lastIndexOf('.');

        if (index < 0)
        {
            return null;
        }

        return forGetter(fieldOrMethod.substring(0, index));
    }

    private final Map<Class<?>, Object> cache;

    private final Accessor stack;
    private final String fieldOrMethod;
    private final boolean getter;
    private final boolean setter;
    private final Class<?>[] parameterTypes;

    private Accessor(Accessor stack, String fieldOrMethod, boolean getter, boolean setter, Class<?>... parameterTypes)
    {
        super();

        cache = new HashMap<Class<?>, Object>(1);

        this.stack = stack;

        int index = fieldOrMethod.lastIndexOf('.');

        if (index >= 0)
        {
            fieldOrMethod = fieldOrMethod.substring(index + 1);
        }

        this.fieldOrMethod = fieldOrMethod;
        this.getter = getter;
        this.setter = setter;
        this.parameterTypes = parameterTypes;
    }

    public Object invoke(Object onObject, Object... parameters)
    {
        if (stack != null)
        {
            onObject = stack.invoke(onObject);
        }

        // TODO check if parameters match the specification

        Class<?> type = onObject.getClass();
        Object accessor = cache.get(type);

        if (accessor == null)
        {
            accessor = resolve(type);

            if (accessor == null)
            {
                throw new IllegalArgumentException(String.format("No suitable field or method %s found in %s",
                    fieldOrMethod, type));
            }

            cache.put(type, accessor);
        }

        if (accessor instanceof Field)
        {
            if (parameters.length == 0)
            {
                try
                {
                    return ((Field) accessor).get(onObject);
                }
                catch (IllegalArgumentException e)
                {
                    throw new IllegalArgumentException("Failed to read value from " + accessor, e);
                }
                catch (IllegalAccessException e)
                {
                    throw new IllegalArgumentException("Failed to access " + accessor, e);
                }
            }

            try
            {
                ((Field) accessor).set(onObject, parameters[0]);
            }
            catch (IllegalArgumentException e)
            {
                throw new IllegalArgumentException("Failed to set value to " + accessor, e);
            }
            catch (IllegalAccessException e)
            {
                throw new IllegalArgumentException("Failed to access " + accessor, e);
            }
        }

        if ((getter) && (parameterTypes.length > 0))
        {
            try
            {
                return TypeConversion.convert(((Method) accessor).invoke(onObject), parameterTypes[0]);
            }
            catch (IllegalAccessException e)
            {
                throw new IllegalArgumentException("Failed to access " + accessor, e);
            }
            catch (IllegalArgumentException e)
            {
                throw new IllegalArgumentException("Failed to invoke " + accessor, e);
            }
            catch (InvocationTargetException e)
            {
                throw new IllegalArgumentException("Failed to invoke " + accessor, e);
            }
        }

        Object[] convertedParameters = new Object[parameters.length];

        for (int i = 0; i < parameters.length; i += 1)
        {
            convertedParameters[i] = TypeConversion.convert(parameters[i], ((Method) accessor).getParameterTypes()[i]);
        }

        try
        {
            return ((Method) accessor).invoke(onObject, convertedParameters);
        }
        catch (IllegalAccessException e)
        {
            throw new IllegalArgumentException("Failed to access " + accessor, e);
        }
        catch (IllegalArgumentException e)
        {
            throw new IllegalArgumentException("Failed to invoke " + accessor, e);
        }
        catch (InvocationTargetException e)
        {
            throw new IllegalArgumentException("Failed to invoke " + accessor, e);
        }
    }

    private Object resolve(Class<?> type)
    {
        if (((getter) || (setter)) && (parameterTypes.length == 1))
        {
            for (Field field : type.getFields())
            {
                if (!Modifier.isPublic(field.getModifiers()))
                {
                    continue;
                }

                if (!fieldOrMethod.equalsIgnoreCase(field.getName()))
                {
                    continue;
                }

                if (parameterTypes.length == 1)
                {
                    if (!TypeConversion.isConvertable(parameterTypes[0], field.getType()))
                    {
                        continue;
                    }
                }

                return field;
            }
        }

        for (Method method : type.getMethods())
        {
            if (!Modifier.isPublic(method.getModifiers()))
            {
                continue;
            }

            if (!isNameMatching(method.getName()))
            {
                continue;
            }

            if (getter)
            {
                if (method.getReturnType() == null)
                {
                    continue;
                }

                if ((parameterTypes.length > 0)
                    && (!TypeConversion.isConvertable(method.getReturnType(), parameterTypes[0])))
                {
                    continue;
                }
            }
            else if (parameterTypes.length != method.getParameterTypes().length)
            {
                continue;
            }

            if (!getter)
            {
                for (int i = 0; i < parameterTypes.length; i += 1)
                {
                    if (!TypeConversion.isConvertable(parameterTypes[i], method.getParameterTypes()[i]))
                    {
                        continue;
                    }
                }
            }

            return method;
        }

        return null;
    }

    private boolean isNameMatching(String name)
    {
        if (getter)
        {
            if (("get" + fieldOrMethod).equalsIgnoreCase(name))
            {
                return true;
            }

            if (("is" + fieldOrMethod).equalsIgnoreCase(name))
            {
                return true;
            }
        }

        if (setter)
        {
            if (("set" + fieldOrMethod).equalsIgnoreCase(name))
            {
                return true;
            }
        }

        return fieldOrMethod.equalsIgnoreCase(name);
    }
}
